<!--
 * Copyright 2009, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>WebGL Shadows</title>
<style>
html, body {
  width: 100%;
  height: 100%;
  border: 0px;
  padding: 0px;
  margin: 0px;
  background-color: red;
}
CANVAS {
  background-color: gray;
}
.fpsContainer {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 2;
  color: gray;
  font-family: sans-serif;
}
.fps {
  color: white;
}
#viewContainer {
  width: 100%;
  height: 100%;
}
</style>
<script src="../tdl/base.js"></script>
<script>
tdl.require('tdl.buffers');
tdl.require('tdl.fast');
tdl.require('tdl.fps');
tdl.require('tdl.log');
tdl.require('tdl.math');
tdl.require('tdl.models');
tdl.require('tdl.primitives');
tdl.require('tdl.programs');
tdl.require('tdl.textures');
tdl.require('tdl.webgl');
window.onload = initialize;

// globals
var gl;                   // the gl context.
var canvas;               // the canvas
var math;                 // the math lib.
var fast;                 // the fast math lib.
var g_fpsTimer;           // object to measure frames per second;
var g_logGLCalls = true   // whether or not to log webgl calls
var g_debug = false;      // whether or not to debug.
var g_drawOnce = false;   // draw just one frame.

// shadowing globals
var g_shadowMapWidth = 512;
var g_shadowMapHeight = 512;
var g_debugTexture = true;

var g_setCountElements = [];

//g_drawOnce = true;
g_debug = true;

var g_eyeSpeed        = 0.1;
var g_eyeHeight       = 60;
var g_eyeRadius       = 60;
var g_groundRadius    = 100;
var g_numColumns      = 20;
var g_columnMinHeight = 10;
var g_columnMaxHeight = 30;
var g_columnMinRadius = 0.4;
var g_columnMaxRadius = 3.0;
var g_radialSubdivisions = 10;
var g_lightWorldDir = [-20, -45, -50];
var g_ambientColor = [0.2, 0.2, 0.2, 1.0];

var g_skyBoxUrls = [
  'assets/skybox_positive_x.png',
  'assets/skybox_negative_x.png',
  'assets/skybox_positive_y.png',
  'assets/skybox_negative_y.png',
  'assets/skybox_positive_z.png',
  'assets/skybox_negative_z.png']

function ValidateNoneOfTheArgsAreUndefined(functionName, args) {
  for (var ii = 0; ii < args.length; ++ii) {
    if (args[ii] === undefined) {
      tdl.error("undefined passed to gl." + functionName + "(" +
                tdl.webgl.glFunctionArgsToString(functionName, args) + ")");
    }
  }
}

function Log(msg) {
  if (g_logGLCalls) {
    tdl.log(msg);
  }
}

function LogGLCall(functionName, args) {
  if (g_logGLCalls) {
    ValidateNoneOfTheArgsAreUndefined(functionName, args)
    tdl.log("gl." + functionName + "(" +
            tdl.webgl.glFunctionArgsToString(functionName, args) + ")");
  }
}

/**
 * Create columns
 */
function setupColumns(depthTexture) {
  var readProgram = tdl.programs.loadProgramFromScriptTags(
      'shadowReadVertexShader',
      'shadowReadFragmentShader');
  var writeProgram = tdl.programs.loadProgramFromScriptTags(
      'shadowWriteVertexShader',
      'shadowWriteFragmentShader');
  var textures = {
    diffuseSampler: tdl.textures.loadTexture('assets/rock-color.png'),
    bumpSampler: tdl.textures.loadTexture('assets/rock-nmap.png'),
    depthSampler: depthTexture,
  };

  var verticalSubdivisions = 2;

  var arrays = {};
  for (var ii = 0; ii < g_numColumns; ++ii) {
    var radius = g_columnMinRadius + tdl.math.pseudoRandom() *
      (g_columnMaxRadius - g_columnMinRadius);
    var height = g_columnMinHeight + tdl.math.pseudoRandom() *
      (g_columnMaxHeight - g_columnMinHeight);
    var x = tdl.math.pseudoRandom() * g_groundRadius - g_groundRadius / 2;
    var y = tdl.math.pseudoRandom() * g_groundRadius - g_groundRadius / 2;

    var columnArrays = tdl.primitives.createCylinder(
        radius,
        height,
        g_radialSubdivisions,
        verticalSubdivisions,
        false,
        true);
    tdl.primitives.addTangentsAndBinormals(columnArrays);
    tdl.primitives.reorient(columnArrays,
        [1, 0, 0, 0,
         0, 1, 0, 0,
         0, 0, 1, 0,
         x, height/2, y, 1]);
    if (ii === 0) {
      arrays = columnArrays;
    } else {
      arrays = tdl.primitives.concat([arrays, columnArrays]);
    }
  };

  return {
    read: new tdl.models.Model(readProgram, arrays, textures),
    write: new tdl.models.Model(writeProgram, arrays, null),
  };
}

/**
 * Sets up the Skybox
 */
function setupSkybox() {
  var textures = {
    skybox: tdl.textures.loadTexture(g_skyBoxUrls)};
  var program = tdl.programs.loadProgramFromScriptTags(
      'skyboxVertexShader',
      'skyboxFragmentShader');
  var arrays = tdl.primitives.createPlane(2, 2, 1, 1);
  delete arrays['normal'];
  delete arrays['texCoord'];
  tdl.primitives.reorient(arrays,
      [1, 0, 0, 0,
       0, 0, 1, 0,
       0,-1, 0, 0,
       0, 0, 0.99, 1]);
  return new tdl.models.Model(program, arrays, textures);
}

/**
 * Sets up the ground
 */
function setupGround(depthTexture) {
  var readProgram = tdl.programs.loadProgramFromScriptTags(
      'shadowReadVertexShader',
      'shadowReadFragmentShader');
  var textures = {
    diffuseSampler: tdl.textures.loadTexture('assets/sand-color.png'),
    bumpSampler: tdl.textures.loadTexture('assets/sand-nmap.png'),
    depthSampler: depthTexture,
  };
  var radius = g_groundRadius;
  var divs = 10;
  var arrays = tdl.primitives.createPlane(radius, radius, divs, divs);
  tdl.primitives.applyPlanarUVMapping(arrays.position, arrays.texCoord);
  tdl.primitives.addTangentsAndBinormals(arrays);
  tdl.primitives.reorient(arrays,
      [1, 0, 0, 0,
       0, 1, 0, 0,
       0, 0, 1, 0,
       0, 0, 0, 1]);
  return new tdl.models.Model(readProgram, arrays, textures);
}

// Generate a debug quad of size (width, height) at screen coords (x, y).
// Coordinates and dimensions are all in NDC space.
function setupDebugTex(tex, x, y, width, height)
{
  var program = tdl.programs.loadProgramFromScriptTags(
      'debugTextureVertexShader',
      'debugTextureFragmentShader');

  var numVertices = 4;
  var positions = new tdl.primitives.AttribBuffer(3, numVertices);
  var texCoords = new tdl.primitives.AttribBuffer(2, numVertices);
  var indices = new tdl.primitives.AttribBuffer(3, 2, 'Uint16Array');

  positions.push([x, y, 0]);
  positions.push([x + width, y, 0]);
  positions.push([x, y + height, 0]);
  positions.push([x + width, y + height, 0]);

  texCoords.push([0,0]);
  texCoords.push([1,0]);
  texCoords.push([0,1]);
  texCoords.push([1,1]);

  indices.push([0,1,2]);
  indices.push([2,1,3]);

  var arrays = {
    position: positions,
    texCoord: texCoords,
    indices: indices,
  };

  var textures = {
    tex : tex,
  };

  return new tdl.models.Model(program, arrays, textures);
};

/**
 * Initializes stuff.
 */
function initialize() {
  math = tdl.math;
  fast = tdl.fast;
  canvas = document.getElementById("canvas");

  g_fpsTimer = new tdl.fps.FPSTimer();
  gl = tdl.webgl.setupWebGL(canvas);
  if (!gl) {
    return false;
  }
  if (g_debug) {
    gl = tdl.webgl.makeDebugContext(gl, undefined, LogGLCall);
  }
  gl.enable(gl.DEPTH_TEST);
  gl.disable(gl.BLEND);
  //gl.enable(gl.CULL_FACE);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

  var shadowFrame = gl.createFramebuffer();
  var depthTexture = new tdl.textures.ExternalTexture2D();
  var depthBuffer = gl.createRenderbuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFrame);
  gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
  gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16,
      g_shadowMapWidth, g_shadowMapHeight);
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
      gl.RENDERBUFFER, depthBuffer);
  gl.bindTexture(gl.TEXTURE_2D, depthTexture.texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, g_shadowMapWidth, g_shadowMapHeight,
      0, gl.RGBA, gl.UNSIGNED_BYTE, null);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D,
    depthTexture.texture, 0);

  Log("--Setup Skybox---------------------------------------");
  var skybox = setupSkybox();
  Log("--Setup Columns--------------------------------------");
  var columns = setupColumns(depthTexture);
  Log("--Setup Ground---------------------------------------");
  var sand = setupGround(depthTexture);

  // Debug texture scale is [0..1], and is a fraction of the screen height.
  var scale = 0.5;
  var aspectRatio = (canvas.clientWidth / canvas.clientHeight) *
    (g_shadowMapHeight / g_shadowMapWidth);
  var debugTex = setupDebugTex(depthTexture, -1, -1, 2*scale / aspectRatio,
      2*scale);

  var then = 0.0;
  var clock = 0.0;
  var fpsElem = document.getElementById("fps");

  var projection = new Float32Array(16);
  var view = new Float32Array(16);
  var world = new Float32Array(16);
  var worldViewProjection = new Float32Array(16);
  var lightViewProjection = new Float32Array(16);
  var viewInverse = new Float32Array(16);
  var viewProjectionInverse = new Float32Array(16);
  var worldInverse = new Float32Array(16);
  var worldInverseTranspose = new Float32Array(16);
  var viewProjection = new Float32Array(16);
  var worldViewProjection = new Float32Array(16);
  var viewInverse = new Float32Array(16);
  var viewProjectionInverse = new Float32Array(16);
  var cameraWorld = new Float32Array(16);
  var cameraWorldInverse = new Float32Array(16);
  var cameraViewProjection = new Float32Array(16);
  var cameraWorldViewProjection = new Float32Array(16);
  var cameraViewInverse = new Float32Array(16);
  var cameraProjectionInverse = new Float32Array(16);
  var eyePosition = new Float32Array(3);
  var target = new Float32Array(3);
  var up = new Float32Array([0,1,0]);
  var lightWorldDir = new Float32Array(g_lightWorldDir);
  var lightWorldPos = new Float32Array(3);
  var lightWorldTarget = new Float32Array([0,0,0]);

  // TODO enne - automatically calculate light position from world bound.
  tdl.fast.normalize(lightWorldPos, lightWorldDir);
  lightWorldPos[0] = -lightWorldPos[0] * g_groundRadius * 2;
  lightWorldPos[1] = -lightWorldPos[1] * g_groundRadius * 2;
  lightWorldPos[2] = -lightWorldPos[2] * g_groundRadius * 2;

  var zero4 = new Float32Array(4);
  var one4 = new Float32Array([1,1,1,1]);
  var colorMult = new Float32Array([1,1,1,1]);

  // Sky uniforms.
  var skyConst = {viewProjectionInverse: viewProjectionInverse};
  var skyPer = {};

  var shadowReadConst = {
    viewInverse: viewInverse,
    lightWorldDir: lightWorldDir,
    lightColor: one4,
    emissive: zero4,
    ambient: g_ambientColor,
    specular: one4,
    shininess: 5,
    specularFactor: 0.3,
  };
  var shadowWriteConst = {
  };

  var shadowReadUniforms = {
    world: world,
    worldViewProjection: worldViewProjection,
    lightViewProjection: lightViewProjection,
    worldInverse: worldInverse,
    worldInverseTranspose: worldInverseTranspose,
  };
  var shadowWriteUniforms = {
    world: world,
    worldViewProjection: lightViewProjection,
    worldInverse: worldInverse,
    worldInverseTranspose: worldInverseTranspose,
  };


  function setupShadowPass() {
    gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFrame);
    gl.viewport(0, 0, g_shadowMapWidth, g_shadowMapHeight);
    gl.clearColor(0,0,0,1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);

    // TODO enne - find the viewport bounds once.
    fast.matrix4.ortho(
        projection,
        -70, 70,
        -70, 70,
        1, 5000);
    fast.matrix4.lookAt(
        view,
        lightWorldPos,
        lightWorldTarget,
        up);
    fast.matrix4.mul(lightViewProjection, view, projection);
    fast.matrix4.inverse(viewInverse, view);
    fast.matrix4.inverse(viewProjectionInverse, lightViewProjection);

    fast.matrix4.translation(world, [0, 0, 0]);
    fast.matrix4.mul(worldViewProjection, world, lightViewProjection);
    fast.matrix4.inverse(worldInverse, world);
    fast.matrix4.transpose(worldInverseTranspose, worldInverse);
  }

  function setupMainPass() {
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0,0,0,0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);

    fast.matrix4.perspective(
        projection,
        math.degToRad(30),
        canvas.clientWidth / canvas.clientHeight,
        1,
        5000);
    fast.matrix4.lookAt(
        view,
        eyePosition,
        target,
        up);
    fast.matrix4.mul(viewProjection, view, projection);
    fast.matrix4.inverse(viewInverse, view);
    fast.matrix4.inverse(viewProjectionInverse, viewProjection);

    fast.matrix4.translation(world, [0, 0, 0]);
    fast.matrix4.mul(worldViewProjection, world, viewProjection);
    fast.matrix4.inverse(worldInverse, world);
    fast.matrix4.transpose(worldInverseTranspose, worldInverse);
  }

  function render() {
    if (!g_drawOnce) {
      requestAnimationFrame(render);
    }
    var now = (new Date()).getTime() * 0.001;
    var elapsedTime;
    if(then == 0.0) {
      elapsedTime = 0.0;
    } else {
      elapsedTime = now - then;
    }
    then = now;

    g_fpsTimer.update(elapsedTime);
    fpsElem.innerHTML = g_fpsTimer.averageFPS;

    clock += elapsedTime;
    eyePosition[0] = Math.sin(clock * g_eyeSpeed) * g_eyeRadius;
    eyePosition[1] = g_eyeHeight;
    eyePosition[2] = Math.cos(clock * g_eyeSpeed) * g_eyeRadius;

    gl.colorMask(true, true, true, true);

    // *** Shadow Pass
    setupShadowPass();

    // Draw Columns
    columns.write.drawPrep(shadowWriteConst);
    columns.write.draw(shadowWriteUniforms);

    // *** Main Pass
    setupMainPass();

    // Draw Skybox
    skybox.drawPrep(skyConst);
    skybox.draw(skyPer);

    sand.drawPrep(shadowReadConst);
    sand.draw(shadowReadUniforms);
    columns.read.drawPrep(shadowReadConst);
    columns.read.draw(shadowReadUniforms);

    // Debug
    if (g_debugTexture) {
      debugTex.drawPrep({});
      debugTex.draw({});
    }

    // turn off logging after 1 frame.
    g_logGLCalls = false;
  }
  render();
  return true;
}
</script>
</head>
<body>
<div class="fpsContainer">
  <div class="fps">fps: <span id="fps"></div>
</div>
<div id="viewContainer">
<canvas id="canvas" width="1024" height="1024" style="width: 100%; height: 100%;"></canvas>
</div>
</body>
<!-- Shadow Write Shader -->
<script id="shadowWriteVertexShader" type="text/something-not-javascript">
uniform mat4 worldViewProjection;
attribute vec4 position;
varying vec4 v_position;
void main() {
  v_position = (worldViewProjection * position);
  gl_Position = v_position;
}
</script>
<script id="shadowWriteFragmentShader" type="text/something-not-javascript">
precision mediump float;
varying vec4 v_position;
void main() {
  float depth = gl_FragCoord.z / gl_FragCoord.w;
  vec4 shift = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);
  gl_FragColor = fract(shift * depth);
}
</script>
<!-- Shadow Read Shader -->
<script id="shadowReadVertexShader" type="text/something-not-javascript">
uniform mat4 worldViewProjection;
uniform mat4 world;
uniform mat4 viewInverse;
uniform mat4 worldInverseTranspose;
uniform mat4 lightViewProjection;
attribute vec4 position;
attribute vec3 normal;
attribute vec2 texCoord;
attribute vec3 tangent;
attribute vec3 binormal;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_tangent;
varying vec3 v_binormal;
varying vec3 v_normal;
varying vec3 v_surfaceToView;
varying vec4 v_lightCoord;
void main() {
  v_texCoord = texCoord;
  v_position = (worldViewProjection * position);
  v_normal = (worldInverseTranspose * vec4(normal, 0)).xyz;
  v_surfaceToView = (viewInverse[3] - (world * position)).xyz;
  v_binormal = (worldInverseTranspose * vec4(binormal, 0)).xyz;
  v_tangent = (worldInverseTranspose * vec4(tangent, 0)).xyz;
  v_lightCoord = (lightViewProjection * (world * position));
  gl_Position = v_position;
}
</script>
<script id="shadowReadFragmentShader" type="text/something-not-javascript">
#ifdef GL_FRAGMENT_PRECISION_HIGH
  precision highp float;
#else
  precision mediump float;
#endif
uniform vec4 lightColor;
uniform vec3 lightWorldDir;
varying vec4 v_position;
varying vec2 v_texCoord;
varying vec3 v_tangent;
varying vec3 v_binormal;
varying vec3 v_normal;
varying vec3 v_surfaceToView;
varying vec4 v_lightCoord;

uniform vec4 emissive;
uniform vec4 ambient;
uniform sampler2D diffuseSampler;
uniform vec4 specular;
uniform sampler2D bumpSampler;
uniform sampler2D depthSampler;
uniform float shininess;
uniform float specularFactor;

vec4 lit(float l ,float h, float m) {
  return vec4(1.0,
              max(l, 0.0),
              (l > 0.0) ? pow(max(0.0, h), m) : 0.0,
              1.0);
}

float getDepth(in vec2 lightCoord)
{
  vec4 shadowCol = texture2D(depthSampler, lightCoord);
  vec4 shift = vec4(1.0/1677216.0, 1.0/65536.0, 1.0/256.0, 1.0);
  float shadowDepth = dot(shadowCol, shift);

  return shadowDepth;
}

void main() {
  vec2 lightCoord = v_lightCoord.xy / 2.0 + 0.5;
  float objDepth = (1.0-v_lightCoord.z) / v_lightCoord.w;
  float shadowDepth = getDepth(lightCoord);
  float shadow = shadowDepth > objDepth ? 1.0 : 0.0;

  vec4 diffuse = texture2D(diffuseSampler, v_texCoord);
  mat3 tangentToWorld = mat3(v_tangent,
                             v_binormal,
                             v_normal);
  vec3 tangentNormal = texture2D(bumpSampler, v_texCoord.xy).xyz -
                                 vec3(0.5, 0.5, 0.5);
  vec3 normal = (tangentToWorld * tangentNormal);
  normal = normalize(normal);
  vec3 surfaceToLight = normalize(-lightWorldDir);
  vec3 surfaceToView = normalize(v_surfaceToView);
  vec3 halfVector = normalize(surfaceToLight + surfaceToView);
  vec4 litR = lit(dot(normal, surfaceToLight),
                    dot(normal, halfVector), shininess) * shadow;
  gl_FragColor = vec4((emissive + lightColor * (ambient * diffuse + diffuse *
          litR.y + specular * litR.z * specularFactor)).rgb, diffuse.a);
}
</script>
<!-- Skybox Shader -->
<script id="skyboxVertexShader" type="text/something-not-javascript">
attribute vec4 position;
varying vec4 v_position;
void main() {
  v_position = position;
  gl_Position = position;
}
</script>
<script id="skyboxFragmentShader" type="text/something-not-javascript">
#ifdef GL_FRAGMENT_PRECISION_HIGH
  precision highp float;
#else
  precision mediump float;
#endif
uniform samplerCube skybox;
uniform mat4 viewProjectionInverse;
varying vec4 v_position;
void main() {
  gl_FragColor = textureCube(
      skybox,
      normalize((viewProjectionInverse * v_position).xyz));
}
</script>
<!-- Debug Texture Shader -->
<script id="debugTextureVertexShader" type="text/something-not-javascript">
attribute vec4 position;
attribute vec2 texCoord;
varying vec2 v_texCoord;
void main() {
    gl_Position = position;
    v_texCoord = texCoord;
}
</script>
<script id="debugTextureFragmentShader" type="text/something-not-javascript">
precision mediump float;
uniform sampler2D tex;
varying vec2 v_texCoord;
void main() {
    gl_FragColor = texture2D(tex, v_texCoord);
}
</script>
</html>


